#define _GNU_SOURCE
#include <stdio.h>
#include <assert.h>
#include <math.h>

#include <object_table.h>
#include <line.h>
#include <utils.h>
#include <debug.h>


/**
 * Allocate a new line.
 *
 * @return the new allocated line object.
 */
line_t *
line_new( void )
{
  line_t *line = calloc( 1, sizeof(*line) );

  OBJECT_TYPE( line ) = OBJECT_TYPE_LINE;

  return line;
}

/**
 * Free memory allocated to line.
 *
 * @param an allocated line object.
 */
void
line_free( line_t *line )
{
  assert( line != NULL );
  free( line );
}

/**
 * Return the string representation of this line object.
 *
 * @note string is malloc()ated, please free() it later.
 *
 * @param line the object to act on.
 *
 * @return string representation.
 */
char *
line_str( line_t *line )
{
  char *s = NULL;
  int r;

  if ( line == NULL )
    r = asprintf( &s, "Line is NULL!" );
  else
    r = asprintf( &s,
		  "Line( depth=%d, color=0x%08x, transp=%0.3f, "
		  "p0=( %d, %d ), p1=( %d, %d ) )",
		  OBJECT_DEPTH( line ),
		  COLOR_TO_ARGB( OBJECT_COLOR( line ) ),
		  OBJECT_TRANSP( line ),
		  line->r.p0.x, line->r.p0.y,
		  line->r.p1.x, line->r.p1.y
		  );
  if ( r < 0 )
    s = NULL;

  return s;
}



void
line_draw_vertical( const uint16_t x, const uint16_t y_min, uint16_t y_max,
		    const surface_t *surface,
		    const color_t color, const float transp )
{
  assert( surface != NULL );
  assert( surface->buffer != NULL );
  assert( y_max >= y_min );

  uint16_t w = surface->size.x;
  uint16_t h = surface->size.y;

  /* doesn't draw outside buffer */
  if ( x >= w )
    return;
  if ( y_max >= h )
    y_max = h;
  if ( y_min > y_max )
    return;

  /* move buffer pointer to (x, y_min ) */
  color_t *buf = &( PIXEL_AT( surface, x, y_min ) );
  uint16_t y;
  for ( y=y_min; y < y_max; y++ )
    {
      pixel_paint( buf, color, transp );
      buf += w;     /* go to the next line, same x */
    }
}

void
line_draw_horizontal( const uint16_t x_min, uint16_t x_max, const uint16_t y,
		      const surface_t *surface,
		      const color_t color, const float transp )
{
  assert( surface != NULL );
  assert( surface->buffer != NULL );
  assert( x_max >= x_min );

  uint16_t w = surface->size.x;
  uint16_t h = surface->size.y;

  /* doesn't draw outside buffer */
  if ( y >= h )
    return;
  if ( x_max >= w )
    x_max = w - 1;
  if ( x_min > x_max )
    return;

  color_t *buf = &( PIXEL_AT( surface, x_min, y ) );
  color_t *end = buf + ( x_max - x_min );

  for (; buf != end; buf ++ )
    pixel_paint( buf, color, transp );
}

/**
 * Internal function to draw lines using the trivial algorithm (floating point)
 *
 * @note It doesn't handle special cases: vertical and horizontal lines
 * @note It assumes x1 > x0
 */
void
line_draw_trivial( uint16_t x0, uint16_t x1, uint16_t y0, uint16_t y1,
		   surface_t *surface, color_t color, float transp )
{
  assert( surface != NULL );
  assert( surface->buffer != NULL );
  assert( x1 > x0 );
  assert( y1 != y0 );

  int dx = x1 - x0;
  int dy = y1 - y0;

  if ( dx > abs( dy ) )
    {
      float m = (float)dy / (float)dx;
      float y = y0;

      uint_fast16_t x;
      for ( x=x0; x <= x1; x++, y += m )
	surface_draw_pixel( surface, x, y, color, transp );
    }
  else
    {
      /* we have more points on y than on x, invert axis */
      float m = (float)dx / (float)dy;

      if ( dy < 0 )
	{
	  dy = -dy; /* useless */
	  swap( x0, x1 );
	  swap( y0, y1 );
	}

      float x = x0;
      uint_fast16_t y;
      for ( y=y0; y <= y1; y++, x += m )
	surface_draw_pixel( surface, x, y, color, transp );
    }
}

/**
 * Internal function to draw lines using the trivial algorithm (floating point)
 *
 * @note It doesn't handle special cases: vertical and horizontal lines
 * @note It assumes x1 > x0
 */
void
line_draw_trivial_antialias( uint16_t x0, uint16_t x1,
			     uint16_t y0, uint16_t y1,
			     surface_t *surface, color_t color, float transp )
{
  assert( surface != NULL );
  assert( surface->buffer != NULL );
  assert( x1 > x0 );
  assert( y1 != y0 );

  int dx = x1 - x0;
  int dy = y1 - y0;

  if ( dx > abs( dy ) )
    {
      float m = (float)dy / (float)dx;
      float y = y0;

      uint_fast16_t x;
      for ( x=x0; x <= x1; x++, y += m )
	{
	  float a = y - floor( y );
	  surface_draw_pixel( surface, x,     y, color,     a + transp );
	  surface_draw_pixel( surface, x, y + 1, color, 1 - a + transp );
	}
    }
  else
    {
      /* we have more points on y than on x, invert axis */
      float m = (float)dx / (float)dy;

      if ( dy < 0 )
	{
	  dy = -dy; /* useless */
	  swap( x0, x1 );
	  swap( y0, y1 );
	}

      float x = x0;
      uint_fast16_t y;
      for ( y=y0; y <= y1; y++, x += m )
	{
	  float a = x - floor( x );
	  surface_draw_pixel( surface,     x, y, color,     a + transp );
	  surface_draw_pixel( surface, x + 1, y, color, 1 - a + transp );
	}
    }
}

/**
 * Internal function to draw lines using the Bresenham (integer only)
 * algorithm.
 *
 * @note It doesn't handle special case: vertical line
 * @note It assumes x1 > x0
 */
void
line_draw_bresenham( uint16_t x0, uint16_t x1, uint16_t y0, uint16_t y1,
		     surface_t *surface, color_t color, float transp )
{
  assert( surface != NULL );
  assert( surface->buffer != NULL );
  assert( x1 > x0 );

  int dx = x1 - x0;
  int dy = y1 - y0;

  int_fast8_t step_y;

  if ( dy >= 0 )
    step_y = 1;
  else
    {
      dy = -dy;
      step_y = -1;
    }

  if ( dx > dy )
    {
      int e = - ( dx / 2 );
      uint_fast16_t x;
      uint_fast16_t y = y0;
      for ( x=x0; x <= x1; x++ )
	{
	  surface_draw_pixel( surface, x, y, color, transp );

	  e += dy;
	  if ( e >= 0 )
	    {
	      y += step_y;
	      e -= dx;
	    }
	}
    }
  else
    {
      int e = - ( dy / 2 );
      uint_fast16_t y;
      uint_fast16_t x = x0;
      for ( y=y0; y != y1; y += step_y )
	{
	  surface_draw_pixel( surface, x, y, color, transp );

	  e += dx;
	  if ( e >= 0 )
	    {
	      x ++;
	      e -= dy;
	    }
	}
    }
}


void (*line_draw_aliased)( uint16_t, uint16_t, uint16_t, uint16_t,
			   surface_t *, color_t, float ) =
  line_draw_bresenham;

void (*line_draw_antialiased)( uint16_t, uint16_t, uint16_t, uint16_t,
			       surface_t *, color_t, float ) =
  line_draw_trivial_antialias;


/**
 * Generic line drawing algoritm.
 *
 * It handle special cases, like vertical and horizontal, does swap
 * to ensure x1 > x0 and chooses the algorithm to do anti-alias or aliased
 * drawings.
 *
 * You may toggle drawing using line_draw_aliased and line_draw_antialiased
 * variables.
 *
 * @param line the line object to draw.
 * @param surface where to draw the object.
 */
void
__line_draw( line_t *line, surface_t *surface )
{
  assert( surface != NULL );
  assert( surface->buffer != NULL );

  uint16_t x0 = line->r.p0.x;
  uint16_t x1 = line->r.p1.x;
  uint16_t y0 = line->r.p0.y;
  uint16_t y1 = line->r.p1.y;
  color_t  color = OBJECT_COLOR( line );
  float transp = OBJECT_TRANSP( line );

  int dx = x1 - x0;
  int dy = y1 - y0;

  if ( dx == 0 )
    {
      /* Special case: vertical line. Line inclination is infinite dy/0 */
      if ( dy >= 0 )
	line_draw_vertical( x0, y0, y1, surface, color, transp );
      else
	line_draw_vertical( x0, y1, y0, surface, color, transp );
    }
  else if ( dy == 0 )
    {
      /* Special case: speed up horizontal line drawing */
      if ( dx >= 0 )
	line_draw_horizontal( x0, x1, y0, surface, color, transp );
      else
	line_draw_horizontal( x1, x0, y0, surface, color, transp );
    }
  else
    {
      if ( OBJECT_DRAW_OPTS( line ) & DRAW_ANTIALIAS )
	{
	  if ( dx >= 0 )
	    line_draw_antialiased( x0, x1, y0, y1, surface, color, transp );
	  else
	    line_draw_antialiased( x1, x0, y1, y0, surface, color, transp );
	}
      else
	{
	  if ( dx >= 0 )
	    line_draw_aliased( x0, x1, y0, y1, surface, color, transp );
	  else
	    line_draw_aliased( x1, x0, y1, y0, surface, color, transp );
	}
    }
}

void
line_draw( line_t *line, surface_t *surface )
{
  surface_reset_visits( surface );
  __line_draw( line, surface );
}
